package woogieunit;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.Vector;
import java.util.HashMap;
import java.io.FileWriter;
import java.io.FileReader;

import org.exolab.castor.xml.MarshalException;
import org.exolab.castor.xml.Marshaller;
import org.exolab.castor.xml.Unmarshaller;
import org.exolab.castor.xml.ValidationException;


import woogieunit.exceptions.*;

import woogieunit.annotations.*;
public class BenchmarkRunner {
	private Vector<Method> benchmarks;
	private HashMap<String, Method> beforeMethods;
	private HashMap<String, Method> afterMethods;
	private HashMap<String, Method> scaleMethods;
	private boolean breakOnAssociatedMethodFailure = true;
	/**Empty constructor*/
	public BenchmarkRunner(){
		//empty
	}
	public Vector<BenchmarkResult> runBenchmarks(Object target){
		return runBenchmarks(target, -1);
	}
	public Vector<BenchmarkResult> runBenchmarks(Object target, int iterationOverride){
		Vector<BenchmarkResult> v = new Vector<BenchmarkResult>();
		beforeMethods = new HashMap<String, Method>();
		afterMethods = new HashMap<String, Method>();
		benchmarks = new Vector<Method>();
		scaleMethods = new HashMap<String, Method>();
		Vector<Method> methods = new Vector<Method>();
		for (Method m : target.getClass().getMethods()){
			methods.add(m);
		}
		Iterator<Method> it = methods.iterator();
		while (it.hasNext()){
			Method m = it.next();
			if (m.isAnnotationPresent(BeforeBenchmark.class)){
				beforeMethods.put(m.getAnnotation(BeforeBenchmark.class).name(), m);
			}
			if (m.isAnnotationPresent(AfterBenchmark.class)){
				afterMethods.put(m.getAnnotation(AfterBenchmark.class).name(), m);
			}
			if (m.isAnnotationPresent(ScalingMethod.class)){
				scaleMethods.put(m.getName(), m);
			}
			if (m.isAnnotationPresent(Benchmark.class)){
				benchmarks.add(m);
			}
		}
		int suiteIterations = 1;
		String suiteName = null;
		if (target.getClass().isAnnotationPresent(BenchmarkSuite.class)){
			BenchmarkSuite bs = target.getClass().getAnnotation(BenchmarkSuite.class);
			suiteIterations = bs.timesToRun();
			suiteName = bs.name();
		}
		for (int suite = 1; suite <= suiteIterations; suite++){
			it = benchmarks.iterator();
			while (it.hasNext()){
				Method m = it.next();
				Benchmark b = m.getAnnotation(Benchmark.class);
				int suggestedRuns = b.timesToRun();
				int delta = 1;
				if (iterationOverride != -1){
					//we want to set our number of iterations to iterationOverride
					//while getting as broad a spread of Benchmarks as possible
					delta = (int) Math.ceil((double)suggestedRuns / (double)iterationOverride);
				}
				for (int i = 1; i <= suggestedRuns; i += delta){
					try{
						BenchmarkResult br;
						br = runBenchmark(target, m, suiteName, i, suite);
						v.add(br);
					}
					catch(InvalidBenchmarkSignatureException ibe){
						v.add(new BenchmarkFailure(ibe, 0, b.name(), suiteName, i, suite));
					}
					catch(IllegalAccessException iae){
						v.add(new BenchmarkFailure(iae, 0, b.name(), suiteName, i, suite));
					}
					catch(InvocationTargetException ite){
						v.add(new BenchmarkFailure(ite, 0, b.name(), suiteName, i, suite));
					}
					catch(Exception e){
						v.add(new BenchmarkFailure(e, 0, b.name(), suiteName, i, suite));
					}
				}
			}
		}
		return v;
	}
	
	public BenchmarkResult runBenchmark(Object target, Method m, String suite, int iteration, int suiteIteration) 
	throws InvalidBenchmarkSignatureException, InvalidScaleSignatureException, AssociatedMethodFailureException, 
	IllegalAccessException, InvocationTargetException, Exception{
		BenchmarkResult retValue;
		//check to see if this is a Benchmark
		if (! m.isAnnotationPresent(Benchmark.class)){
			throw new InvalidBenchmarkSignatureException(target.getClass().getName() + "." + 
					m.getName() + " is not described as Benchmark");
		}
		//check to see if there is an invalid declared ScalingMethod
		if (m.isAnnotationPresent(Scales.class)){
			if (! scaleMethods.containsKey(m.getAnnotation(Scales.class).scalingMethod())){
				throw new InvalidScaleSignatureException( m.getAnnotation(Scales.class).scalingMethod() + 
						" is not present in " + target.getClass().getName() + " as a ScalingMethod");
			}
		}
		else{
			Class[] parameters = m.getParameterTypes();
			boolean noScales = true; //we start by assuming there are no Scales
			boolean allScales = true; //we start by assuming everything has Scales
			for (Class t : parameters){
				if (t.isAnnotationPresent(Scales.class)){
					noScales = false;
					if (! scaleMethods.containsKey(((Scales)t.getAnnotation(Scales.class)).scalingMethod())){
						throw new InvalidScaleSignatureException( m.getAnnotation(Scales.class).scalingMethod() + 
								" is not present in " + target.getClass().getName() + " as a ScalingMethod");
					}
					else{
						Method p = scaleMethods.get(((Scales)t.getAnnotation(Scales.class)).scalingMethod());
						if(!t.equals(p.getReturnType())){
							Class[] c = p.getReturnType().getClasses();
							boolean classMatch = false;
							for (Class cc: c){
								if (t.equals(cc)){
									classMatch = true;
									break;
								}
							}
							if (!classMatch){
								throw new InvalidScaleSignatureException( m.getName() + 
										": Using scaling method: " + p.getName() + " (" + 
										((Scales)t.getAnnotation(Scales.class)).scalingMethod() + 
										"). Parameter mismatch. Scaling method returns " + t.getName() + 
										", parameter is a " + p.getReturnType().getName());								
							}
						}
					}
				}
				else{
					allScales = false;
				}
			}
			if (!noScales && !allScales){
				throw new InvalidScaleSignatureException( target.getClass().getName() + 
						"." + m.getName() + " does not declare Scales on all its parameters");
			}
			
		}
		Benchmark b = m.getAnnotation(Benchmark.class);
		if (beforeMethods.containsKey(b.name())){
			try{
				beforeMethods.get(b.name()).invoke(target, new Object[0]);
			}
			catch(InvocationTargetException ite){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("BeforeMethod " + beforeMethods.get(b.name()).getName() + 
						" failed", ite);
				}
			}
			catch(IllegalAccessException iae){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("BeforeMethod " + beforeMethods.get(b.name()).getName() + 
						" failed", iae);
				}
			}
			catch(Exception e){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("BeforeMethod " + beforeMethods.get(b.name()).getName() + 
							" failed", e);
				}
			}
		}
		Class[] parmTypes = m.getParameterTypes();
		Object[] parms = new Object[parmTypes.length];
		if (m.isAnnotationPresent(Scales.class)){
			Method s = scaleMethods.get(m.getAnnotation(Scales.class).scalingMethod());
			try{
				Object[] args = new Object[1];
				args[0] = iteration;
				parms = (Object[]) s.invoke(target, args);
			}
			catch(IllegalAccessException iae){
				throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
						m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, iae);
			}
			catch(InvocationTargetException ite){
				throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
						m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, ite);
			}
			catch(Exception e){
				throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
						m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, e);
			}
		}
		for (int i = 0; i < parmTypes.length; i++){
			if (parmTypes[i].isAnnotationPresent(Scales.class)){
				Method s = (scaleMethods.get((((Scales)parmTypes[i].getAnnotation(Scales.class))).scalingMethod()));
				try{
					Object[] args = new Object[1];
					args[0] = iteration;
					parms[i] = s.invoke(target, args);
				}
				catch(IllegalAccessException iae){
					throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
							m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, iae);
				}
				catch(InvocationTargetException ite){
					throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
							m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, ite);
				}
				catch(Exception e){
					throw new AssociatedMethodFailureException("Scaling method " + s.getName() + " (" + 
							m.getAnnotation(Scales.class).scalingMethod() + ") failed on iteration " + iteration, e);
				}
			}
		}
		long ctime = System.nanoTime();
		try{
			m.invoke(target, parms);
		}
		catch(IllegalAccessException iae){
			 throw iae;
		}
		catch(InvocationTargetException ite){
			 throw ite;
		}
		catch(Exception e){
			 throw e;
		}
		long etime = System.nanoTime();
		long dtime = etime - ctime;
		if (afterMethods.containsKey(b.name())){
			try{
				afterMethods.get(b.name()).invoke(target, new Object[0]);
			}
			catch(InvocationTargetException ite){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("AfterMethod " + afterMethods.get(b.name()).getName() + 
						" failed", ite);
				}
			}
			catch(IllegalAccessException iae){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("AfterMethod " + afterMethods.get(b.name()).getName() + 
						" failed", iae);
				}
			}
			catch(Exception e){
				if (breakOnAssociatedMethodFailure){
					throw new AssociatedMethodFailureException("AfterMethod " + afterMethods.get(b.name()).getName() + 
							" failed", e);
				}
			}
		}
		retValue = new BenchmarkResult(dtime, b.name(), suite, iteration, suiteIteration);
		return retValue;
	}
	public Vector<BenchmarkResult> runBenchmarks(String fullClassName)
	throws ClassNotFoundException, InstantiationException, IllegalAccessException  {
		Class x;
		Object y;
		try{
			x = Class.forName(fullClassName);
			y = x.newInstance();
			return runBenchmarks(y);
		}
		catch (ClassNotFoundException cnfe){
			throw cnfe;
		}
		catch (InstantiationException ie){
			throw ie;
		}
		catch (IllegalAccessException iae){
			throw iae;
		}
		
	}
	public static void printResults(Vector<BenchmarkResult> v, PrintStream p){
		Iterator<BenchmarkResult> it = v.iterator();
		while (it.hasNext()){
			p.println( it.next().toString());
		}
	}
	public static void writeResults(Vector<BenchmarkResult> v, String fileName) 
	throws IOException{
		try{
			FileWriter x = new FileWriter(fileName);
			Marshaller.marshal(v, x);
		}
		catch (IOException ioe){
			throw ioe;
		}
		catch (ValidationException ve){
			throw new IOException("Marshal exception thrown by Castor");
		}
		catch (MarshalException me){
			throw new IOException("Marshal exception thrown by Castor");
		}
	}
	
	public static Vector<BenchmarkResult> loadResults(String fileName)
	throws IOException{
			FileReader fr = new FileReader(fileName);
			return loadResults(fr);
	}
	
	public static Vector<BenchmarkResult> loadResults(FileReader fr)
	throws IOException{
		try{
			Vector<BenchmarkResult> v = new Vector<BenchmarkResult>();
			v = (Vector<BenchmarkResult>) Unmarshaller.unmarshal(v.getClass(), fr);
			return v;
		}
		catch (ValidationException ve){
			throw new IOException("Marshal exception thrown by Castor");
		}
		catch (MarshalException me){
			throw new IOException("Marshal exception thrown by Castor");
		}
	}
}
